<?php

namespace App\Utils;

use App\Exception\LockTimeoutException;
use Carbon\Carbon;
use Hyperf\Redis\Redis;
use Hyperf\Stringable\Str;
use RedisException;

use function Hyperf\Config\config;

/**
 * Redis锁
 */
class RedisLock
{
    private Redis $redis;
    private string $name;
    private int $seconds;

    private ?string $owner;

    private int $sleepMilliseconds = 250;

    public function __construct(Redis $redis, string $name, int $seconds, ?string $owner = null)
    {
        if (is_null($owner)) {
            $owner = Str::random();
        }

        $this->redis = $redis;
        $this->name = config('cache.default.prefix') . $name;
        $this->seconds = $seconds;
        $this->owner = $owner;
    }

    /**
     * 尝试获取锁
     * @param callable|null $callback
     * @return mixed
     * @throws RedisException
     */
    public function get(?callable $callback = null): mixed
    {
        $result = $this->acquire();

        if ($result && is_callable($callback)) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }

        return $result;
    }

    /**
     * 尝试在给定的秒数内获取锁。
     * @param int $seconds
     * @param callable|null $callback
     * @return mixed
     * @throws LockTimeoutException
     * @throws RedisException
     */
    public function block(int $seconds, ?callable $callback = null): mixed
    {
        $starting = $this->currentTime();

        while (!$this->acquire()) {
            usleep($this->sleepMilliseconds * 1000);

            if ($this->currentTime() - $seconds >= $starting) {
                throw new LockTimeoutException;
            }
        }

        if (is_callable($callback)) {
            try {
                return $callback();
            } finally {
                $this->release();
            }
        }

        return true;
    }

    /**
     * 获得锁
     * @return bool
     * @throws RedisException
     */
    public function acquire(): bool
    {
        if ($this->seconds > 0) {
            return $this->redis->set($this->name, $this->owner, ['NX', 'EX' => $this->seconds]) == true;
        } else {
            return $this->redis->setnx($this->name, $this->owner) === 1;
        }
    }

    /**
     * 释放锁
     * @return bool
     * @throws RedisException
     */
    public function release(): bool
    {
        $scripts = <<<'LUA'
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
LUA;
        return (bool)$this->redis->eval($scripts, [$this->name, $this->owner], 1);
    }

    /**
     * 强制释放
     * @return bool
     * @throws RedisException
     */
    public function forceRelease(): bool
    {
        return $this->redis->del($this->name) === 1;
    }

    /**
     * 指定被阻塞的锁获取尝试之间的睡眠毫秒数。
     * @param $milliseconds
     * @return $this
     */
    public function betweenBlockedAttemptsSleepFor($milliseconds): self
    {
        $this->sleepMilliseconds = $milliseconds;

        return $this;
    }

    /**
     * 当前时间
     * @return int
     */
    private function currentTime(): int
    {
        return Carbon::now()->timestamp;
    }
}
